/* Copyright (c) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.sk.gsync.app;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gdata.client.media.ResumableGDataFileUploader;
import com.google.gdata.client.uploader.FileUploadData;
import com.google.gdata.client.uploader.ProgressListener;
import com.google.gdata.client.uploader.ResumableHttpFileUploader;
import com.google.gdata.data.Link;
import com.google.gdata.data.docs.DocumentListEntry;
import com.google.gdata.data.docs.DocumentListFeed;
import com.google.gdata.data.media.MediaFileSource;
import com.google.gdata.util.ServiceException;

/**
 * A console aplication to demonstrate interaction with Google Docs API to
 * upload/update large media files using Resumable Upload protocol.
 */
public class DocumentResumableUploadDemo {

    /** Default document list feed url. */
    private static final String DEFAULT_DOCLIST_FEED_URL =
        "https://docs.google.com/feeds/default/private/full";

    /** Default create-media-url for uploading documents */
    private static final String DEFAULT_RESUMABLE_UPLOAD_URL =
        "https://docs.google.com/feeds/upload/create-session/default/private/full";

    /** Maximum number of concurrent uploads */
    private static final int MAX_CONCURRENT_UPLOADS = 10;

    /** Time interval at which upload task will notify about the progress */
    private static final int PROGRESS_UPDATE_INTERVAL = 1000;

    /** Max size for each upload chunk */
    private static final int DEFAULT_CHUNK_SIZE = 10000000;

    /**
     * Welcome message, introducing the program.
     */
    private static final String[] WELCOME_MESSAGE =
        {
                "",
                "This is a demo of the resumable upload feature for Docs GData API",
                "Using this interface, you can upload/update large documents.",
                "" };

    private static final String APPLICATION_NAME =
        "JavaGDataClientSampleAppV3.0";

    private static final String[] USAGE_MESSAGE =
        {"Usage: java DocumentResumableUploadDemo.jar --username <user> --password <pass>", };

    private static final String[] COMMAND_HELP_MESSAGE = {
            "Commands:",
            "   list [object_type]                                 "
                + "                             [[list objects]]",
            "   upload <file_path1:title1> ... <file_pathN:titleN> "
                + "[<chunk_size_in_byes>]       [[uploads set of files]]",
            "   update <ducument_id> <updated_file_path> [<chunk_size_in_bytes>] "
                + "               [[updates content of an object]]", };

    /** Instance of {@link DocumentList} */
    private final DocumentList docs;

    /** Steam to print status messages to. */
    PrintStream output;

    /**
     * Constructor.
     * 
     * @param docs
     *            {@link DocumentList} for interface to DocList API service.
     * @param out
     *            printstream to output status messages to.
     */
    DocumentResumableUploadDemo(final DocumentList docs,
            final PrintStream out) {
        this.docs = docs;
        output = out;
    }

    /**
     * Prints out a message.
     * 
     * @param msg
     *            the message to be printed.
     */
    private static void printMessage(final String[] msg) {
        for (String s : msg) {
            System.out.println(s);
        }
    }

    private String[] parseCommand(final String command) {
        return command.trim().split(" ");
    }

    /**
     * Prints out the specified document entry.
     * 
     * @param doc
     *            the document entry to print.
     */
    public void printDocumentEntry(final DocumentListEntry doc) {
        StringBuffer outputBuffer = new StringBuffer();

        outputBuffer.append(" -- " + doc.getTitle().getPlainText() + " ");
        if (!doc.getParentLinks().isEmpty()) {
            for (Link link : doc.getParentLinks()) {
                outputBuffer.append("[" + link.getTitle() + "] ");
            }
        }
        outputBuffer.append(doc.getResourceId());

        output.println(outputBuffer);
    }

    /**
     * Uploads given collection of files. The call blocks until all uploads are
     * done.
     * 
     * @param url
     *            create-session url for initiating resumable uploads for
     *            documents API.
     * @param files
     *            list of absolute filepaths to files to upload.
     * @param chunkSize
     *            max size of each upload chunk.
     */
    public Collection<DocumentListEntry> uploadFiles(final String url,
            final List<String> files, final int chunkSize)
            throws IOException, ServiceException, InterruptedException {
        // Create a listener
        FileUploadProgressListener listener =
            new FileUploadProgressListener();
        // Pool for handling concurrent upload tasks
        ExecutorService executor =
            Executors.newFixedThreadPool(MAX_CONCURRENT_UPLOADS);
        // Create {@link ResumableGDataFileUploader} for each file to upload
        List<ResumableGDataFileUploader> uploaders = Lists.newArrayList();
        for (String fileName : files) {
            MediaFileSource mediaFile = getMediaFileSource(fileName);
            ResumableGDataFileUploader uploader =
                new ResumableGDataFileUploader.Builder(docs.docsService,
                    new URL(url), mediaFile, null /* empty meatadata */)
                    .title(mediaFile.getName()).chunkSize(chunkSize)
                    .executor(executor)
                    .trackProgress(listener, PROGRESS_UPDATE_INTERVAL)
                    .build();
            uploaders.add(uploader);
        }
        // attach the listener to list of uploaders
        listener.listenTo(uploaders);

        // Start the upload
        for (ResumableGDataFileUploader uploader : uploaders) {
            uploader.start();
        }

        // wait for uploads to complete
        while (!listener.isDone()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ie) {
                listener.printResults();
                throw ie; // rethrow
            }
        }

        // print upload results
        listener.printResults();

        // return list of uploaded entries
        return listener.getUploaded();
    }

    private MediaFileSource getMediaFileSource(final String fileName) {
        File file = new File(fileName);
        MediaFileSource mediaFile =
            new MediaFileSource(file, DocumentListEntry.MediaType
                .fromFileName(file.getName()).getMimeType());
        return mediaFile;
    }

    /**
     * Execute 'list' command.
     */
    private void executeList(final String[] args)
            throws IOException, ServiceException, DocumentListException {
        DocumentListFeed feed = null;
        String msg = "";

        switch (args.length) {
        case 1:
            msg = "List of docs: ";
            feed = docs.getDocsListFeed("all");
            break;
        case 2:
            msg = "List of all " + args[1] + ": ";
            feed = docs.getDocsListFeed(args[1]);
            break;
        case 3:
            if (args[1].equals("folder")) {
                msg = "Contents of folder_id '" + args[2] + "': ";
                feed = docs.getFolderDocsListFeed(args[2]);
            }
            break;
        }

        if (feed != null) {
            output.println(msg);
            for (DocumentListEntry entry : feed.getEntries()) {
                printDocumentEntry(entry);
            }
        } else {
            printMessage(COMMAND_HELP_MESSAGE);
        }
    }

    /**
     * Execute 'upload' command.
     */
    private void executeUpload(final String[] args)
            throws IOException, ServiceException, InterruptedException {
        if (args.length > 1) {
            int chunkSize = DEFAULT_CHUNK_SIZE;
            List<String> files = Lists.newArrayList();
            for (int index = 1; index < args.length; index++) {
                String arg = args[index];
                if (index < args.length - 1) {
                    files.add(arg);
                    continue;
                }
                // Last argument can be a file or chunk size
                try {
                    chunkSize = Integer.parseInt(arg);
                } catch (NumberFormatException nfe) {
                    files.add(arg);
                }
            }
            uploadFiles(DEFAULT_RESUMABLE_UPLOAD_URL, files, chunkSize);
            output.println("Finished upload");
        }
    }

    /**
     * Execute 'update' command.
     */
    private void executeUpdate(final String[] args)
            throws IOException, ServiceException, InterruptedException {
        String docIdToUpdate = args[1];
        String filePath = args[2];

        // retrieve latest entry
        DocumentListEntry currentEntry =
            docs.docsService.getEntry(new URL(DEFAULT_DOCLIST_FEED_URL
                + "/" + docIdToUpdate), DocumentListEntry.class);

        MediaFileSource mediaFile = getMediaFileSource(filePath);
        ResumableGDataFileUploader uploader =
            new ResumableGDataFileUploader.Builder(docs.docsService,
                mediaFile, currentEntry)
                .title(mediaFile.getName())
                .requestType(
                    ResumableGDataFileUploader.RequestType.UPDATE_MEDIA_ONLY)
                .build();

        uploader.start();

        // wait for upload to complete
        while (!uploader.isDone()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ie) {
                output.println("Media update interrupted at: "
                    + String.format("%3.0f", uploader.getProgress() * 100)
                    + "%");
                throw ie; // rethrow
            }
        }
        DocumentListEntry updatedEntry =
            uploader.getResponse(DocumentListEntry.class);

        output.println("Finished update");
    }

    private boolean executeCommand(final BufferedReader reader)
            throws IOException, ServiceException, InterruptedException {

        output.println("Enter a command");

        try {
            String command = reader.readLine();
            if (command == null) {
                return false;
            }

            String[] args = parseCommand(command);
            String name = args[0];

            if (name.equals("list")) {
                executeList(args);
            } else if (name.equals("upload")) {
                executeUpload(args);
            } else if (name.equals("update")) {
                executeUpdate(args);
            } else if (name.startsWith("q") || name.startsWith("exit")) {
                return false;
            } else if (name.equals("help")) {
                printMessage(COMMAND_HELP_MESSAGE);
            } else {
                output
                    .println("Unknown command. Type 'help' for list of commands");
            }
        } catch (DocumentListException e) {
            e.printStackTrace();
        }
        return true;
    }

    void run() throws IOException, ServiceException, InterruptedException {
        printMessage(WELCOME_MESSAGE);
        printMessage(COMMAND_HELP_MESSAGE);

        BufferedReader reader =
            new BufferedReader(new InputStreamReader(System.in));

        while (executeCommand(reader)) {
        }
    }

    public static void main(final String[] args)
            throws DocumentListException, IOException, ServiceException,
            InterruptedException {

        // SimpleCommandLineParser parser = new SimpleCommandLineParser(args);
        // String user = parser.getValue("username", "user", "u");
        // String password = parser.getValue("password", "pass", "p");
        // boolean help = parser.containsKey("help", "h");
        //
        // if (help || (user == null || password == null)) {
        // printMessage(USAGE_MESSAGE);
        // System.exit(1);
        // }
        //
        // // authenticate
        // DocumentList docs = new DocumentList(APPLICATION_NAME);
        // docs.login(user, password);
        //
        // DocumentResumableUploadDemo demo =
        // new DocumentResumableUploadDemo(docs, System.out);
        // demo.run();
        // System.exit(1);
    }

    /**
     * A {@link ProgressListener} implementation to track upload progress. The
     * listener can track multiple uploads at the same time. Use {@link #isDone}
     * to check if all uploads are completed and use {@link #getUploaded} to
     * access results of successful uploads.
     */
    private class FileUploadProgressListener implements ProgressListener {

        private final Collection<ResumableGDataFileUploader> trackedUploaders =
            Lists.newArrayList();

        private int pendingRequests;

        Map<String, DocumentListEntry> uploaded = Maps.newHashMap();

        Map<String, String> failed = Maps.newHashMap();

        boolean processed;

        public FileUploadProgressListener() {
            pendingRequests = 0;
        }

        public void listenTo(
                final Collection<ResumableGDataFileUploader> uploaders) {
            trackedUploaders.addAll(uploaders);
            pendingRequests = trackedUploaders.size();
        }

        public synchronized void progressChanged(
                final ResumableHttpFileUploader uploader) {
            String fileId =
                ((FileUploadData) uploader.getData()).getFileName();
            switch (uploader.getUploadState()) {
            case COMPLETE:
            case CLIENT_ERROR:
                pendingRequests -= 1;
                output.println(fileId + ": Completed");
                break;
            case IN_PROGRESS:
                output.println(fileId + ":"
                    + String.format("%3.0f", uploader.getProgress() * 100)
                    + "%");
                break;
            case NOT_STARTED:
                output.println(fileId + ":" + "Not Started");
                break;
            }
        }

        public synchronized boolean isDone() {
            // not done if there are any pending requests.
            if (pendingRequests > 0) {
                return false;
            }
            // if all responses are processed., nothing to do.
            if (processed) {
                return true;
            }
            // check if all response streams are available.
            for (ResumableGDataFileUploader uploader : trackedUploaders) {
                if (!uploader.isDone()) {
                    return false;
                }
            }
            // process all responses
            for (ResumableGDataFileUploader uploader : trackedUploaders) {
                String fileId =
                    ((FileUploadData) uploader.getData()).getFileName();
                switch (uploader.getUploadState()) {
                case COMPLETE:
                    try {
                        DocumentListEntry entry =
                            uploader.getResponse(DocumentListEntry.class);
                        uploaded.put(fileId, entry);
                    } catch (IOException e) {
                        failed.put(fileId,
                            "Upload completed, but unexpected error "
                                + "reading server response");
                    } catch (ServiceException e) {
                        failed
                            .put(fileId,
                                "Upload completed, but failed to parse server response");
                    }
                    break;
                case CLIENT_ERROR:
                    failed.put(fileId,
                        "Failed at " + uploader.getProgress());
                    break;
                }
            }
            processed = true;
            output.println("All requests done");
            return true;
        }

        public synchronized Collection<DocumentListEntry> getUploaded() {
            if (!isDone()) {
                return null;
            }
            return uploaded.values();
        }

        public synchronized void printResults() {
            if (!isDone()) {
                return;
            }
            output.println("Result: " + uploaded.size() + ", "
                + failed.size());
            if (uploaded.size() > 0) {
                output.println(" Successfully Uploaded:");
                for (Map.Entry<String, DocumentListEntry> entry : uploaded
                    .entrySet()) {
                    printDocumentEntry(entry.getValue());
                }
            }
            if (failed.size() > 0) {
                output.println(" Failed to upload:");
                for (Map.Entry entry : failed.entrySet()) {
                    output.println("  " + entry.getKey() + ":"
                        + entry.getValue());
                }
            }
        }

        /**
         * Prints out the specified document entry.
         * 
         * @param doc
         *            the document entry to print.
         */
        public void printDocumentEntry(final DocumentListEntry doc) {
            StringBuffer buffer = new StringBuffer();

            buffer.append(" -- " + doc.getTitle().getPlainText() + " ");
            if (!doc.getParentLinks().isEmpty()) {
                for (Link link : doc.getParentLinks()) {
                    buffer.append("[" + link.getTitle() + "] ");
                }
            }
            buffer.append(doc.getResourceId());

            output.println(buffer);
        }

    }

}
